package timing.ukulele.seal.service.minio;

import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

/**
 * 存储对象相关操作封装
 */
@Service
public class ObjectService {
    private final MinioClient client;

    public ObjectService(MinioClient client) {
        this.client = client;
    }

    /**
     * 以流的形式下载一个对象
     * <p>
     * 注意：读取完毕或者出现异常后要关闭流
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @return InputStream  流
     * @throws IOException               连接异常
     * @throws InvalidKeyException       不合法的access key或者secret key
     * @throws NoSuchAlgorithmException  找不到相应的签名算法
     * @throws InsufficientDataException 在读到相应length之前就得到一个EOFException
     * @throws InternalException         内部错误
     * @throws ErrorResponseException    执行失败异常
     * @throws InvalidResponseException
     * @throws ServerException
     * @throws XmlParserException
     */
    public InputStream getObject(String bucketName, String objectName) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException,
            NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        // 调用statObject()来判断对象是否存在。
        // 如果不存在, statObject()抛出异常,
        // 否则则代表对象存在。
        client.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        // 获取"objectName"的输入流。
        InputStream stream = client.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
        return stream;
    }

    /**
     * 下载对象指定区域的字节数组做为流。（断点下载）
     * 注意：读取完毕或者出现异常后要关闭流
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @param offset     是起始字节的位置
     * @param length     要读取的长度 (可选，如果无值则代表读到文件结尾)
     * @return InputStream 流
     * @throws IOException               连接异常
     * @throws InvalidKeyException       不合法的access key或者secret key
     * @throws NoSuchAlgorithmException  找不到相应的签名算法
     * @throws InsufficientDataException 在读到相应length之前就得到一个EOFException
     * @throws InternalException         内部错误
     * @throws ErrorResponseException    执行失败异常
     * @throws InvalidResponseException
     * @throws ServerException
     * @throws XmlParserException
     */
    public InputStream getObject(String bucketName, String objectName, Long offset, Long length) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException,
            NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        // 调用statObject()来判断对象是否存在。
        // 如果不存在, statObject()抛出异常,
        // 否则则代表对象存在。
        client.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        // 获取指定offset和length的objectName的输入流。
        InputStream stream = length == null ? client.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).build())
                : client.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).build());
        return stream;
    }

    /**
     * 在给定的存储桶中获取整个加密对象的数据作为InputStream，然后用传入的master key解密和加密对象关联的content key。
     * 然后创建一个含有InputStream和Cipher的CipherInputStream。这个Cipher被初始为用于使用content key进行解密，
     * 所以CipherInputStream会在返回数据前，尝试读取数据并进行解密。所以read()方法返回的是处理过的原始对象数据。
     * <p>
     * CipherInputStream必须用完关闭，否则连接不会被释放
     * <p>
     * <p>
     * 注意：读取完毕或者出现异常后要关闭流
     * </p>
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @param key        SecretKey类型的数据
     * @return CipherInputStream 流
     * @throws IOException               连接异常
     * @throws InvalidKeyException       不合法的access key或者secret key
     * @throws NoSuchAlgorithmException  找不到相应的签名算法
     * @throws InsufficientDataException 在读到相应length之前就得到一个EOFException
     * @throws InternalException         内部错误
     * @throws ErrorResponseException    执行失败异常
     * @throws ServerException
     * @throws InvalidResponseException
     * @throws XmlParserException
     */
    public GetObjectResponse getObject(String bucketName, String objectName, SecretKey key) throws NoSuchAlgorithmException, InvalidKeyException, ServerException, InsufficientDataException,
            InternalException, IOException, InvalidResponseException, XmlParserException, ErrorResponseException {
        // 调用statObject()来判断对象是否存在。
        // 如果不存在, statObject()抛出异常,
        // 否则则代表对象存在。
        client.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());

        ServerSideEncryptionCustomerKey serverSideEncryptionCustomerKey = new ServerSideEncryptionCustomerKey(key);
        // 获取对象数据
        GetObjectResponse response = client.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).ssec(serverSideEncryptionCustomerKey).build());

        return response;
    }

    /**
     * 通过InputStream上传对象
     * <p>
     * 单个对象的最大大小限制在5TB。putObject在对象大于5MiB时，
     * 自动使用multiple parts方式上传。这样，当上传失败时，客户端只需要上传未成功的部分即可（类似断点上传）。
     * 上传的对象使用MD5SUM签名进行完整性验证。
     * </p>
     *
     * @param bucketName  存储桶名称
     * @param objectName  存储桶里的对象名称
     * @param stream      要上传的流
     * @param contentType Content type
     * @throws IOException               连接异常
     * @throws InvalidKeyException       不合法的access key或者secret key
     * @throws NoSuchAlgorithmException  找不到相应的签名算法
     * @throws InsufficientDataException 在读到相应length之前就得到一个EOFException
     * @throws InternalException         内部错误
     * @throws ErrorResponseException    执行失败异常
     * @throws ServerException
     * @throws InvalidResponseException
     * @throws XmlParserException
     */
    public void putObject(String bucketName, String objectName, InputStream stream, String contentType) throws IOException, ServerException, InsufficientDataException, InternalException,
            InvalidResponseException, InvalidKeyException, NoSuchAlgorithmException, XmlParserException, ErrorResponseException {
        client.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, stream.available(), -1).contentType(contentType).build());
        stream.close();
    }

    /**
     * <p>
     * 拿到流的数据，使用随机生成的content key进行加密，并上传到指定存储桶中。同时将加密后的content key和iv做为加密对象有header也上传到存储桶中。
     * content key使用传入到该方法的master key进行加密。如果对象大于5MB,客户端会自动进行multi part上传
     * </p>
     * <p>
     * 对象使用随机生成的key进行加密，然后这个用于加密数据的key又被由仅被client知道的master key(封装在encryptionMaterials对象里)进行加密。
     * 这个被加密后的key和IV做为对象的header和加密后的对象一起被上传到存储服务上。
     * </p>
     *
     * @param bucketName  存储桶名称
     * @param objectName  存储桶里的对象名称
     * @param stream      要上传的流
     * @param contentType Content type
     * @param key         用AES初使化的对象SecretKey
     * @throws IOException               连接异常
     * @throws InvalidKeyException       不合法的access key或者secret key
     * @throws NoSuchAlgorithmException  找不到相应的签名算法
     * @throws InsufficientDataException 在读到相应length之前就得到一个EOFException
     * @throws InternalException         内部错误
     * @throws ErrorResponseException    执行失败异常
     * @throws ServerException
     * @throws InvalidResponseException
     * @throws XmlParserException
     */
    public void putObject(String bucketName, String objectName, InputStream stream, String contentType, SecretKey key) throws IOException, ServerException, InsufficientDataException,
            InternalException, InvalidResponseException, InvalidKeyException, NoSuchAlgorithmException, XmlParserException, ErrorResponseException {
        ServerSideEncryptionCustomerKey serverSideEncryptionCustomerKey = new ServerSideEncryptionCustomerKey(key);
        client.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, stream.available(), -1).contentType(contentType).sse(serverSideEncryptionCustomerKey).build());
        stream.close();
    }

    /**
     * 获取对象的元数据
     * 一般也用来检测元对象是否存在
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @return
     * @throws IOException               连接异常
     * @throws InvalidKeyException       不合法的access key或者secret key
     * @throws NoSuchAlgorithmException  找不到相应的签名算法
     * @throws InsufficientDataException 在读到相应length之前就得到一个EOFException
     * @throws InternalException         内部错误
     * @throws ErrorResponseException    执行失败异常
     * @throws InvalidResponseException
     * @throws ServerException
     * @throws XmlParserException
     */
    public StatObjectResponse statObject(String bucketName, String objectName) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException,
            NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        // 获得对象的元数据。
        StatObjectResponse statObjectResponse = client.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        return statObjectResponse;
    }

    /**
     * 删除一个对象
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @throws IOException               连接异常
     * @throws InvalidKeyException       不合法的access key或者secret key
     * @throws NoSuchAlgorithmException  找不到相应的签名算法
     * @throws InsufficientDataException 在读到相应length之前就得到一个EOFException
     * @throws InternalException         内部错误
     * @throws ErrorResponseException    执行失败异常
     * @throws InvalidResponseException
     * @throws ServerException
     * @throws XmlParserException
     */
    public void removeObject(String bucketName, String objectName) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException,
            ServerException, InternalException, XmlParserException, ErrorResponseException {
        client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    /**
     * 删除多个对象
     *
     * @param bucketName  存储桶名称
     * @param objectNames 含有要删除的多个object名称的迭代器对象
     * @return Iterable<Result < DeleteError>>:an iterator of Result DeleteError
     */
    public Iterable<Result<DeleteError>> removeObject(String bucketName, Iterable<DeleteObject> objectNames) {
        Iterable<Result<DeleteError>> results = client.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objectNames).build());
        return results;
    }

}
